package entity;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import util.DateUtil;
import exception.AlmiranteException;

@Entity
@Table(name="iteracao", schema="almirante")
public class Iteracao {
	
	@Id
	@SequenceGenerator(name="iteracao_seq", sequenceName="iteracao_id_iteracao_seq", schema="almirante")
	@GeneratedValue(strategy=GenerationType.AUTO, generator="iteracao_seq")
	@Column(name="id_iteracao")
	private Long id;
	
	@Column(name="numero", nullable=false)
	private Integer numero;
	
	@ManyToOne
	@JoinColumns({
		@JoinColumn(name="id_projeto", referencedColumnName="id_projeto"),
		@JoinColumn(name="cod_fase_rup", referencedColumnName="cod_fase_rup")
	})
	private Fase fase;
	
	@Column(name="nome", length=30, nullable=false)
	private String nome;
	
	@Column(name="duracao", nullable=false)
	private Integer duracao;
	
	@Embedded
	@AttributeOverrides({
		@AttributeOverride(name="inicio", column=@Column(name="inicio_estimado")),
		@AttributeOverride(name="fim", column=@Column(name="fim_estimado"))
	})
	private Periodo periodoEstimado;
	
	@Column(name="horas_estimadas", nullable=false)
	private Integer horasEstimadas;
	
	@Column(name="detalhes", length=1000)
	private String detalhes;
	
	@Column(name="outros_recursos", length=1000)
	private String outrosRecursos;
	
	private SituacaoIteracao situacao;
	
	@Temporal(TemporalType.DATE)
	@Column(name="inicio_realizado")
	private Date inicioRealizado;
	
	@Temporal(TemporalType.DATE)
	@Column(name="fim_realizado")
	private Date fimRealizado;
	
	@ManyToMany
	@JoinTable(name="iteracao_requisito", schema="almirante", 
		joinColumns=@JoinColumn(name="id_iteracao"), 
		inverseJoinColumns=@JoinColumn(name="id_requisito"))
	private Set<Requisito> requisitos = new HashSet<Requisito>();
	
	@ManyToMany
	@JoinTable(name="iteracao_casouso", schema="almirante", 
		joinColumns=@JoinColumn(name="id_iteracao"), 
		inverseJoinColumns=@JoinColumn(name="id_casouso"))
	private Set<CasoUso> casosUso = new HashSet<CasoUso>(); 
	
	@OneToMany(mappedBy="iteracao", cascade=CascadeType.ALL, orphanRemoval=true)
	@OrderBy("numero")
	private List<Atividade> atividades = new ArrayList<Atividade>();
	
	@ManyToMany
	@JoinTable(name="iteracao_recurso", schema="almirante", 
		joinColumns=@JoinColumn(name="id_iteracao"), 
		inverseJoinColumns=@JoinColumn(name="id_recurso"))
	@OrderBy("nomeRecurso")
	private Set<Recurso> recursos = new HashSet<Recurso>();
	
	public static enum SituacaoIteracao {
		CRIADA("Criada"), 
		EM_PLANEJAMENTO("Em Planejamento"),
		PLANEJADA("Planejada"),
		EM_ANDAMENTO("Em Andamento"), 
		CONCLUIDA("Concluída"),
		EM_REPLANEJAMENTO("Em Replanejamento");
		
		private final String descricao;
		
		private SituacaoIteracao(String descricao) {
			this.descricao = descricao;
		}

		public String getDescricao() {
			return descricao;
		}
	}
	
	public Iteracao() {
		this.situacao = SituacaoIteracao.CRIADA;
	}
	
	public Iteracao(String nome, Integer duracao) {
		this();
		this.nome = nome;
		this.duracao = duracao;
	}
	
	void setFase(Fase fase) {
		if (this.fase != null)
			throw new AlmiranteException("Iteração já está vinculada a uma fase.");
		this.fase = fase;
		this.recursos.add(projeto().getGerenteProjetos());
	}
	
	public Date getInicio() {
		return periodoEstimado.getInicio();
	}
	
	public Date getFim() {
		return periodoEstimado.getFim();
	}
	
	Periodo getPeriodoEstimado() {
		return this.periodoEstimado;
	}
	
	void setPeriodoEstimado(Periodo periodo) {
		if (situacao.equals(SituacaoIteracao.CONCLUIDA))
			throw new AlmiranteException("Não é possível alterar o perído estimado da iteração, pois já se " +
					"encontra Concluída.");
		
		int diferenca = this.periodoEstimado != null ?
				DateUtil.diferencaEmDias(periodoEstimado.getInicio(), periodo.getInicio()) : 0;
		
		this.periodoEstimado = periodo;
		if (diferenca != 0) {
			// recalcula as datas das atividades
			List<Atividade> atividadesOrdenadasPorInicio = new ArrayList<Atividade>(atividades);
			Collections.sort(atividadesOrdenadasPorInicio, new Comparator<Atividade>() {
				@Override
				public int compare(Atividade a1, Atividade a2) {
					return a1.getInicioEstimado().compareTo(a2.getInicioEstimado());
				}
			});
			
			for (Atividade atividade: atividadesOrdenadasPorInicio) {
				if (!atividade.possuiPredecessora() || diferenca < 0) {
					atividade.setPeriodo(atividade.getPeriodo().adiantarEmDias(diferenca));					
				}
			}
		}
	}
	
	public void setDuracao(Integer duracao) {
		if (situacao.equals(SituacaoIteracao.CONCLUIDA))
			throw new AlmiranteException("Não é possível alterar a duração da iteração, pois já se " +
					"encontra Concluída.");
		this.duracao = duracao;
		if (this.fase != null) {
			this.projeto().recalcularPeriodoIteracoes();			
		}
	}
	public Integer getDuracao() {
		return this.duracao;
	}
	
	public String getNome() {
		return nome;
	}
	
	public void setNome(String descricao) {
		this.nome = descricao;
	}
	
	public Fase getFase() {
		return this.fase;
	}
	
	void setNumero(Integer numero) {
		this.numero = numero;
	}
	
	public Integer getNumero() {
		return numero;
	}
	
	public String getDetalhes() {
		return detalhes;
	}

	public void setDetalhes(String detalhes) {
		this.detalhes = detalhes;
	}

	public Long getId() {
		return this.id;
	}

	public Integer getHorasEstimadas() {
		return horasEstimadas;
	}

	public void setHorasEstimadas(Integer horasEstimadas) {
		this.horasEstimadas = horasEstimadas;
	}
	
	public void adicionarRequisito(Requisito requisito) {
		verificaSeSituacaoPermiteAlteracao();
		
		if (!projeto().possuiRequisito(requisito))
			throw new AlmiranteException("Requisito não pertence ao projeto.");
		
		requisitos.add(requisito);
	}
	
	void verificaSeSituacaoPermiteAlteracao() {
		if (!situacaoPermiteAlteracao())
			throw new AlmiranteException("Dados da iteração não podem ser alterados, " +
					"pois se encontra na situacao: " + situacao.descricao);
	}
	
	private boolean situacaoPermiteAlteracao() {
		return situacao.equals(SituacaoIteracao.EM_PLANEJAMENTO) || situacao.equals(SituacaoIteracao.EM_REPLANEJAMENTO);
	}
	
	public Set<Requisito> getRequisitos() {
		return Collections.unmodifiableSet(requisitos);
	}
	
	public List<Atividade> getAtividades() {
		return atividades;
	}
	
	public Atividade criarAtividade() {
		verificaSeSituacaoPermiteAlteracao();
		
		Atividade atividade = new Atividade(this, getInicio(), getInicio());
		atividades.add(atividade);
		atividade.setNumero(atividades.size());
		return atividade;
	}
	
	public Atividade getAtividade(Integer numero) {
		if (numero < 1 || numero > atividades.size())
			throw new AlmiranteException("Iteração não possui atividade #" + numero);
		return atividades.get(numero - 1);
	}
	
	public void removerAtividade(Integer numero) {
		verificaSeSituacaoPermiteAlteracao();
		Atividade atividade = getAtividade(numero);
		atividade.removerTodasPredecessoras();
		atividades.remove(atividade);
		
		Integer novoNumero = 0;
		for (Atividade a : atividades) {
			a.setNumero(++novoNumero);
		}
	}
	
	public SituacaoIteracao getSituacao() {
		return situacao;
	}

	public void planejar() {
		if (!podePlanejar())
			throw new AlmiranteException("Iteração já não pode ser planejada, pois se encontra na situação: " + situacao.descricao);
		this.situacao = SituacaoIteracao.EM_PLANEJAMENTO;
	}
	
	public boolean podePlanejar() {
		return situacao.equals(SituacaoIteracao.CRIADA) || 
				situacao.equals(SituacaoIteracao.EM_PLANEJAMENTO) || 
				situacao.equals(SituacaoIteracao.PLANEJADA);
	}
	
	public void iniciar(Date dataInicio) {
		if (!situacao.equals(SituacaoIteracao.PLANEJADA))
			throw new AlmiranteException("Iteração " + fase.getFaseRup().getNome() + " #" + numero + " ainda não foi planejada!");
		
		if (possuiIteracaoAnteriorNaoConcluida()) {
			throw new AlmiranteException("Iteração #" + numero + " não pode ser iniciada, " +
					"pois existem iterações anteriores não concluídas.");			
		}
		
		this.situacao = SituacaoIteracao.EM_ANDAMENTO;
		this.inicioRealizado = dataInicio;
		if (!atividades.isEmpty()) {
			for (Atividade atividade : atividades) {
				atividade.iniciar();
			}
		}
	}
	
	public boolean podeIniciar() {
		return situacao.equals(SituacaoIteracao.PLANEJADA) && !possuiIteracaoAnteriorNaoConcluida();
	}
	
	public Integer getHorasPlanoIteracao() {
		int horas = 0;
		if (!atividades.isEmpty()) {
			for (Atividade atividade : atividades) {
				horas += atividade.getHorasEstimadas();
			}
		}
		
		return horas;
	}
	
	public Integer getHorasRealizadas() {
		int horas = 0;
		if (!atividades.isEmpty()) {
			for (Atividade atividade : atividades) {
				horas += atividade.getHorasRealizadas();
			}
		}
		
		return horas;
	}
	
	public void adicionarRecurso(Recurso recurso) {
		verificaSeSituacaoPermiteAlteracao();
		
		if (!projeto().possuiRecurso(recurso)) {
			throw new AlmiranteException(recurso.getNomeRecurso() + " não pertence ao projeto.");
		}
			
		recursos.add(recurso);
	}
	
	public void removerRecurso(Recurso recurso) {
		verificaSeSituacaoPermiteAlteracao();
		
		if (!atividades.isEmpty()) {
			for (Atividade atividade : atividades) {
				if (recurso.equals(atividade.getResponsavel())) {
					throw new AlmiranteException("Recurso não pode ser removido, pois já está à atividade(s).");
				}
			}
		}
		
		recursos.remove(recurso);
	}
	
	public Set<Recurso> getRecursos() {
		return Collections.unmodifiableSet(recursos);
	}
	
	public boolean possuiRequisito(Requisito requisito) {
		return requisitos.contains(requisito);
	}
	
	public boolean possuiRecurso(Recurso recurso) {
		return recursos.contains(recurso);
	}
	
	public void removerRequisito(Requisito requisito) {
		verificaSeSituacaoPermiteAlteracao();
		
		if (!atividades.isEmpty()) {
			for (Atividade atividade : atividades) {
				if (atividade.possuiRequisito(requisito))
					throw new AlmiranteException("Requisito não pode ser removido, pois já está vinculado à atividade #" + atividade.getNumero());
			}
		}

		requisitos.remove(requisito);
	}
	
	public void concluirPlanejamento() {
		if (!atividades.isEmpty()) {
			for (Atividade atividade : atividades) {
				if (atividade.getResponsavel() == null)
					throw new AlmiranteException("Não é possível concluir o planejamento, " +
							"pois a atividade #" + atividade.getNumero() + " não foi atribuída a nenhum recurso.");
			}
		}
		
		if (!this.situacao.equals(SituacaoIteracao.EM_PLANEJAMENTO))
			throw new AlmiranteException("Iteração não está Em Planejamento!");
		
		this.situacao = SituacaoIteracao.PLANEJADA;
	}
	
	private boolean possuiIteracaoAnteriorNaoConcluida() {
		for (Iteracao iteracao : projeto().iteracoesAnterioresA(this)) {
			if (!iteracao.getSituacao().equals(SituacaoIteracao.CONCLUIDA)) {
				return true;
			}
		}
		
		return false;
	}
	
	public String getOutrosRecursos() {
		return outrosRecursos;
	}

	public void setOutrosRecursos(String outrosRecursos) {
		this.outrosRecursos = outrosRecursos;
	}
	
	public void concluir(Date dataConclusao) {
		if (!podeConcluir())
			throw new AlmiranteException("Iteração não pode ser concluída, pois não se encontra Em Andamento.");
		situacao = SituacaoIteracao.CONCLUIDA;
		fimRealizado = dataConclusao;
		if (ultimaIteracao()) {
			projeto().concluir();
		}
	}
	
	public boolean podeConcluir() {
		return situacao.equals(SituacaoIteracao.EM_ANDAMENTO);
	}
	
	public Date getInicioRealizado() {
		return inicioRealizado;
	}
	
	public Date getFimRealizado() {
		return fimRealizado;
	}
	
	public void replanejar() {
		if (!podeReplanejar())
			throw new AlmiranteException("Iteração não pode ser replanejada, " +
					"pois se encontra na situação: " + situacao.descricao);
		situacao = SituacaoIteracao.EM_REPLANEJAMENTO;
		
	}
	
	public boolean podeReplanejar() {
		return situacao.equals(SituacaoIteracao.EM_ANDAMENTO) || situacao.equals(SituacaoIteracao.EM_REPLANEJAMENTO);
	}
	
	public void concluirReplanejamento() {
		if (!situacao.equals(SituacaoIteracao.EM_REPLANEJAMENTO))
			throw new AlmiranteException("Não é possível concluir o replanejamento, " +
					"pois a situacação da iteração se encontra: " + situacao.descricao);
		situacao = SituacaoIteracao.EM_ANDAMENTO;
		if (!atividades.isEmpty()) {
			for (Atividade a : atividades) {
				if (a.podeIniciar()) {
					a.iniciar();
				}
			}
		}
	}
	
	private boolean ultimaIteracao() {
		Iteracao iteracao = projeto().ultimaIteracao();
		return this.equals(iteracao);
	}
	
	private Projeto projeto() {
		return fase.getProjeto();
	}
	
	public void adicionarCasoUso(CasoUso casoUso) {
		verificaSeSituacaoPermiteAlteracao();
		
		if (!projeto().possuiCasoUso(casoUso))
			throw new AlmiranteException("Caso de Uso " + casoUso.getNomeCasoUso() +
					" não pertence ao proejeto.");
		
		casosUso.add(casoUso);
		if (!casoUso.getListRequisito().isEmpty()) {
			for (Requisito requisito : casoUso.getListRequisito()) {
				if (!possuiRequisito(requisito)) {
					requisitos.add(requisito);
				}
			}
		}
	}
	
	public boolean possuiCasoUso(CasoUso casoUso) {
		if (!casosUso.isEmpty()) {
			return casosUso.contains(casoUso);
		}
		return false;
	}
	
	public void removerCasoUso(CasoUso casoUso) {
		verificaSeSituacaoPermiteAlteracao();
		
		if (!atividades.isEmpty()) {
			for (Atividade atividade : atividades) {
				if (atividade.possuiCasoUso(casoUso))
					throw new AlmiranteException("Caso de Uso não pode ser removido, pois já está vinculado à atividade #" + atividade.getNumero());
			}
		}
		
		casosUso.remove(casoUso);
	}
	
}
